use super::{
    err, err_value, json, save_file, throw_err, Client, ECode, Part, Resp, VBase, VMap, Value,
};
use calamine::{open_workbook, Reader, Xlsx};
use std::{collections::HashMap, ffi::OsStr, fs, io, path::Path};
use tracing::{instrument, warn};

#[instrument(skip_all, name = "do_cmd")]
pub async fn do_cmd(part: &mut Part, client: &Client) -> Resp {
    let fname = save_file(part).await?;

    warn!("esop file: {fname}");

    let (line, tsn) = line_tech(&fname)?;
    let set = col_set(&line, client).await?;

    let f = unzip_file(&fname)?;
    let buf = read_file(&f)?;

    let head = &buf[0];
    let mut keym = Vec::new();

    for d in head.iter() {
        if let Some(k) = set.get(d.as_str()) {
            keym.push(k);
        }
    }

    let mut iter = buf.iter();
    iter.next();

    let mut esop: HashMap<String, Vec<Value>> = HashMap::new();

    for item in iter {
        let mut tmp = HashMap::new();
        let val: Vec<_> = keym.iter().zip(item.iter()).collect();

        for k in val.iter() {
            tmp.insert(k.0.to_string(), k.1);
        }

        let wsn = tmp
            .get("wsn")
            .ok_or(err_value(ECode::JsonFormat, "no para: wsn"))?
            .to_string();

        let tmp = json!(tmp);

        match esop.get_mut(&wsn) {
            Some(v) => v.push(tmp),
            None => {
                esop.insert(wsn.to_string(), vec![tmp]);
            }
        }
    }

    for (_, v) in esop.iter_mut() {
        v.sort_by_key(|k| {
            let step = match k.k_str("step") {
                Some(v) => v,
                None => return 0,
            };

            let step: i32 = match step.parse() {
                Ok(v) => v,
                Err(_) => 0,
            };

            step
        });
    }

    let cmd = json!({
        "line": line,
        "k": tsn,
        "v": esop,
    });

    client.cfgdb(&cmd, "esop/add").await
}

fn unzip_file(fname: &str) -> Result<String, Value> {
    warn!("unzip file: {fname}");

    let path = Path::new(fname);
    let parent = path
        .parent()
        .ok_or(err_value(ECode::FileRead, "path error"))?;

    let fh = fs::File::open(fname).or_else(|e| err!(ECode::FileRead, e))?;

    let mut archive = zip::ZipArchive::new(fh).or_else(|e| err!(ECode::FileRead, e))?;

    for i in 0..archive.len() {
        let mut file = archive.by_index(i).or_else(|e| err!(ECode::FileRead, e))?;

        let p = match file.enclosed_name() {
            Some(path) => path,
            None => continue,
        };

        let outpath = parent.join(p);

        if file.is_dir() {
            fs::create_dir_all(&outpath).or_else(|e| err!(ECode::FileRead, e))?;
        } else {
            if let Some(p) = outpath.parent() {
                if !p.exists() {
                    fs::create_dir_all(p).or_else(|e| err!(ECode::FileRead, e))?;
                }
            }

            let mut outfile = fs::File::create(&outpath).or_else(|e| err!(ECode::FileRead, e))?;
            io::copy(&mut file, &mut outfile).or_else(|e| err!(ECode::FileRead, e))?;
        }
    }

    Ok(parent.display().to_string())
}

#[instrument(skip_all, name = "read_file")]
fn read_file(f: &str) -> Result<Vec<Vec<String>>, Value> {
    let path = Path::new(f).join("tech/step.xlsx");

    warn!("read file: {}", path.display());

    let mut workbook: Xlsx<_> = open_workbook(path).or_else(|e| err!(ECode::FileRead, e))?;

    let sheet = workbook
        .worksheet_range_at(0)
        .ok_or(err_value(ECode::FileRead, "sheet error"))?
        .or_else(|e| err!(ECode::FileRead, e))?;

    let row = sheet.rows();
    let mut ret = Vec::new();

    for r in row {
        let mut col = Vec::new();
        for c in r {
            col.push(c.to_string());
        }

        ret.push(col);
    }

    if ret.is_empty() {
        throw_err!(ECode::EmptyData, "no esop data");
    }

    Ok(ret)
}

fn line_tech(fname: &str) -> Result<(String, String), Value> {
    let mut it = Path::new(fname).iter();
    if it.next() != Some(OsStr::new(&std::path::MAIN_SEPARATOR.to_string())) {
        throw_err!(ECode::FileRead, "path error");
    }

    if it.next() != Some(OsStr::new("files")) {
        throw_err!(ECode::FileRead, "path error: /files");
    }

    let line = it
        .next()
        .ok_or(err_value(ECode::FileRead, "path error, no line"))?
        .to_str()
        .ok_or(err_value(ECode::FileRead, "path error, no line"))?
        .to_string();

    if it.next() != Some(OsStr::new("tech")) {
        throw_err!(ECode::FileRead, "path error: /tech");
    }

    let tsn = it
        .next()
        .ok_or(err_value(ECode::FileRead, "path error, no line"))?
        .to_str()
        .ok_or(err_value(ECode::FileRead, "path error, no line"))?
        .to_string();

    Ok((line, tsn))
}

async fn col_set(line: &str, client: &Client) -> Result<HashMap<String, String>, Value> {
    let cmd = json!({ "line": line });
    let body = client.cfgdb(&cmd, "field/list/esop").await?;

    let col = body.ck_array("data")?;

    let mut set = HashMap::new();
    let mut is_wsn = false;
    let mut is_step = false;

    for d in col.iter() {
        let d = d.ckobj()?;

        let sn = d.ck_str_empty_trim("sn")?;
        let name = d.ck_str_empty_trim("name")?;

        if sn == "wsn" {
            is_wsn = true;
        }

        if sn == "step" {
            is_step = true;
        }

        set.insert(name.to_string(), sn.to_string());
    }

    if !is_wsn {
        throw_err!(ECode::Para, "esop need sn: wsn");
    }

    if !is_step {
        throw_err!(ECode::Para, "esop need sn: step");
    }

    Ok(set)
}
